package com.manolo.client.connection;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import com.manolo.event.dispatcher.CardDispatcher;
import com.manolo.event.dispatcher.ChatDispatcher;
import com.manolo.event.dispatcher.DispatchType;
import com.manolo.event.dispatcher.GameDispatcher;
import com.manolo.event.dispatcher.SystemDispatcher;
import com.manolo.event.entity.Message;
import com.manolo.client.entity.*;
import com.manolo.event.HandCardEvent;
import com.manolo.event.ChatEvent;
import com.manolo.event.EventType;
import com.manolo.event.GameStartEvent;
import com.manolo.event.PlayerListEvent;
import com.manolo.event.SystemEvent;
import com.manolo.event.listener.ChatListener;
import com.manolo.event.listener.SystemListener;

public class Connection implements Runnable, SystemListener, ChatListener {
	// The host:port combination to connect to
	private InetAddress hostAddress;
	private int port;

	// The selector we'll be monitoring
	private Selector selector;

	// We'll use only one socket
	SocketChannel socket;

	// The buffer into which we'll read data when it's available
	private ByteBuffer readBuffer = ByteBuffer.allocate(8192);

	// A list of PendingChange instances
	private List pendingChanges = new LinkedList();

	// Maps a SocketChannel to a list of ByteBuffer instances
	private List queue = new LinkedList();

	public Connection(String hostAddress, int port) throws IOException {
		this.hostAddress = InetAddress.getByName(hostAddress);
		this.port = port;
		this.selector = this.initSelector();
		this.socket = this.initiateConnection();

		SystemDispatcher.getInstance().addExternalListener(this);
		ChatDispatcher.getInstance().addExternalListener(this);
	}

	public void send(byte[] data) throws IOException {

		// And queue the data we want written
		synchronized (this.queue) {
			queue.add(ByteBuffer.wrap(data));
		}
		synchronized (this.pendingChanges) {
			this.pendingChanges.add(new ChangeRequest(socket,
					ChangeRequest.CHANGEOPS, SelectionKey.OP_WRITE));
		}
		// Finally, wake up our selecting thread so it can make the required
		// changes
		this.selector.wakeup();
	}

	public void run() {
		while (true) {
			try {
				// Process any pending changes
				synchronized (this.pendingChanges) {
					Iterator changes = this.pendingChanges.iterator();
					while (changes.hasNext()) {
						ChangeRequest change = (ChangeRequest) changes.next();
						switch (change.type) {
						case ChangeRequest.CHANGEOPS:
							SelectionKey key = change.socket
									.keyFor(this.selector);
							if (key != null && key.isValid())
								key.interestOps(change.ops);
							break;
						case ChangeRequest.REGISTER:
							change.socket.register(this.selector, change.ops);
							break;
						}
					}
					this.pendingChanges.clear();
				}

				// Wait for an event one of the registered channels
				this.selector.select();

				// Iterate over the set of keys for which events are available
				Iterator selectedKeys = this.selector.selectedKeys().iterator();
				while (selectedKeys.hasNext()) {
					SelectionKey key = (SelectionKey) selectedKeys.next();
					selectedKeys.remove();

					if (!key.isValid()) {
						continue;
					}

					// Check what event is available and deal with it
					if (key.isReadable()) {
						this.read(key);
					} else if (key.isWritable()) {
						this.write(key);
					} else if (key.isConnectable()) {
						this.finishConnection(key);
					}

				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	private void read(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();

		// Clear out our read buffer so it's ready for new data
		this.readBuffer.clear();

		// Attempt to read off the channel
		int numRead;
		try {
			numRead = socketChannel.read(this.readBuffer);
		} catch (IOException e) {
			// The remote forcibly closed the connection, cancel
			// the selection key and close the channel.
			key.cancel();
			socketChannel.close();
			return;
		}

		if (numRead == -1) {
			// Remote entity shut the socket down cleanly. Do the
			// same from our end and cancel the channel.
			key.channel().close();
			key.cancel();
			return;
		}
		// Handle the response
		this.handleResponse(socketChannel, this.readBuffer.array(), numRead);
	}
	
	private List<String> verifyStructMessage(String in){
		List<String> msgs = new ArrayList<String>();
		// tratamento
		if (in.contains("}{\"event\":\"")) {
			String[] values = in.split("\"event\":\"");
			for (String val : values) {
				if (val.length() > 12) {
					val = val.replace("}{", "}");
					msgs.add("{\"event\":\"" + val);

				}
			}
		} else {
			msgs.add(in);
		}
		
		return msgs;
	}

	private void handleResponse(SocketChannel socketChannel, byte[] data,
			int numRead) throws IOException {
		// Make a correctly sized copy of the data before handing it
		// to the client
		byte[] rspData = new byte[numRead];
		System.arraycopy(data, 0, rspData, 0, numRead);

		String in = new String(rspData);
		Gson gson = new Gson();

		//try {

			List<String> msgs = verifyStructMessage(in);
			for (String val : msgs) {

				Message msg = gson.fromJson(val, Message.class);
				switch (msg.getEventType()) {
				case SYSTEM:

					if (msg.getMethod().equals("nameAccepted")) {
						SystemEvent evt = gson.fromJson(msg.getEvent(),
								SystemEvent.class);
						SystemDispatcher.getInstance()
								.dispatchNameAcceptedEvent(evt,
										DispatchType.LOCAL);
					} else if (msg.getMethod().equals("nameDeclined")) {
						SystemEvent evt = gson.fromJson(msg.getEvent(),
								SystemEvent.class);
						SystemDispatcher.getInstance()
								.dispatchNameDeclinedEvent(evt,
										DispatchType.LOCAL);
					} else if (msg.getMethod().equals("setPlayerList")) {
						PlayerListEvent evt = gson.fromJson(msg.getEvent(),
								PlayerListEvent.class);
						SystemDispatcher.getInstance().dispatchSetPlayerList(
								evt, DispatchType.LOCAL);
					} else if (msg.getMethod().equals("playerJoined")) {
						PlayerListEvent evt = gson.fromJson(msg.getEvent(),
								PlayerListEvent.class);
						SystemDispatcher.getInstance().dispatchPlayerJoined(
								evt, DispatchType.LOCAL);
					} else if (msg.getMethod().equals("playerLeft")) {
						PlayerListEvent evt = gson.fromJson(msg.getEvent(),
								PlayerListEvent.class);
						SystemDispatcher.getInstance().dispatchPlayerLeft(evt,
								DispatchType.LOCAL);
					}
					break;

				case CHAT:
					ChatEvent chat = gson.fromJson(msg.getEvent(),
							ChatEvent.class);

					if (msg.getMethod().equals("globalChat"))
						ChatDispatcher.getInstance().dispatchChatEvent(chat,
								DispatchType.LOCAL);
					break;
				case CARD:
					HandCardEvent cardEvent = gson.fromJson(msg.getEvent(),
							HandCardEvent.class);
					CardDispatcher.getInstance().dispatchHandCardEvent(
							cardEvent, DispatchType.LOCAL);
					break;
				case GAME:
					if (msg.getMethod().equals("startGame")) {
						GameStartEvent evt = gson.fromJson(msg.getEvent(),
								GameStartEvent.class);
						GameDispatcher.getInstance().dispatchGameStartEvent(
								evt, DispatchType.LOCAL);
					}

				default:
					break;
				}
			}
		//} catch (Exception e) {
		//	System.out.println("Invalid Client Message: [" + in + "]");
		//}
	}

	public void close() {
		try {
			socket.close();
			socket.keyFor(this.selector).cancel();
			SystemDispatcher.getInstance().removeExternalListener(this);
			ChatDispatcher.getInstance().removeExternalListener(this);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (Throwable e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	private void write(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();

		synchronized (this.queue) {

			// Write until there's not more data ...
			while (!queue.isEmpty()) {

				ByteBuffer buf = (ByteBuffer) queue.get(0);
				socketChannel.write(buf);
				if (buf.remaining() > 0) {
					// ... or the socket's buffer fills up
					break;
				}
				queue.remove(0);
			}

			if (queue.isEmpty()) {
				// We wrote away all data, so we're no longer interested
				// in writing on this socket. Switch back to waiting for
				// data.
				key.interestOps(SelectionKey.OP_READ);
			}
		}
	}

	private void finishConnection(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();

		// Finish the connection. If the connection operation failed
		// this will raise an IOException.
		try {
			socketChannel.finishConnect();
		} catch (IOException e) {
			// Cancel the channel's registration with our selector
			System.out.println(e);
			key.cancel();
			return;
		}

		// Register an interest in writing on this channel
		key.interestOps(SelectionKey.OP_WRITE);
	}

	private SocketChannel initiateConnection() throws IOException {
		// Create a non-blocking socket channel
		SocketChannel socketChannel = SocketChannel.open();
		socketChannel.configureBlocking(false);

		// Kick off connection establishment
		socketChannel
				.connect(new InetSocketAddress(this.hostAddress, this.port));

		// Queue a channel registration since the caller is not the
		// selecting thread. As part of the registration we'll register
		// an interest in connection events. These are raised when a channel
		// is ready to complete connection establishment.
		synchronized (this.pendingChanges) {
			this.pendingChanges.add(new ChangeRequest(socketChannel,
					ChangeRequest.REGISTER, SelectionKey.OP_CONNECT));
		}

		return socketChannel;
	}

	private Selector initSelector() throws IOException {
		// Create a new selector
		return SelectorProvider.provider().openSelector();
	}

	@Override
	public void defineName(SystemEvent event) {

		Gson gson = new Gson();

		Message msg = new Message();

		msg.setEvent(gson.toJson(event));
		msg.setEventType(EventType.SYSTEM);
		msg.setMethod("defineName");

		try {
			this.send(gson.toJson(msg).getBytes());
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	@Override
	public void nameAccepted(SystemEvent event) {
		// TODO Auto-generated method stub

	}

	@Override
	public void nameDeclined(SystemEvent event) {
		// TODO Auto-generated method stub

	}

	@Override
	public void GlobalChat(ChatEvent event) {
		Gson gson = new Gson();

		Message msg = new Message();

		msg.setEvent(gson.toJson(event));
		msg.setEventType(EventType.CHAT);
		msg.setMethod("globalChat");

		try {
			this.send(gson.toJson(msg).getBytes());
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	@Override
	public void getPlayerList() {
		Gson gson = new Gson();

		Message msg = new Message();

		msg.setEventType(EventType.SYSTEM);
		msg.setMethod("getPlayerList");

		try {
			this.send(gson.toJson(msg).getBytes());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void setPlayerList(PlayerListEvent event) {
		// TODO Auto-generated method stub

	}

	@Override
	public void playerJoined(PlayerListEvent event) {
		// TODO Auto-generated method stub

	}

	@Override
	public void playerLeft(PlayerListEvent event) {
		// TODO Auto-generated method stub

	}
}